﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
using System.Xaml;

// Human Interface Project/Link
// Copyright © 2010 The Little Software Company
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

namespace Hip {
	public class LinkExtension: MarkupExtension {
		private static Dictionary<string, object> targetIndex = new Dictionary<string, object>();

		private string _member;

		/// <summary>
		/// Identifies a property or method used to update the source of the link when modified by editable controls.
		/// </summary>
		public string SetBack { get; set; }
		public UpdateSourceTrigger UpdateSourceTrigger { get; set; }
		// add Mode?

		public LinkExtension() {
		}

		public LinkExtension( string member ) {
			_member = member;
		}

		public override object ProvideValue( IServiceProvider serviceProvider ) {
			IXamlTypeResolver typeResolver = (IXamlTypeResolver)serviceProvider.GetService( typeof( IXamlTypeResolver ) );
			IProvideValueTarget valueProvider = (IProvideValueTarget)serviceProvider.GetService( typeof( IProvideValueTarget ) );
			// currently three built-in targets supported:
			object @this = ( (IRootObjectProvider)serviceProvider.GetService( typeof( IRootObjectProvider ) ) ).RootObject;
			object @target = valueProvider.TargetObject; // analogous to RelativeSource.Self
			//object @property = valueProvider.TargetProperty;
			object @context = null;
			Type thisType = @this.GetType();

			if ( @target is FrameworkElement )
				@context = ( (FrameworkElement)@target ).DataContext;
			else if ( @target is FrameworkContentElement )
				@context = ( (FrameworkContentElement)@target ).DataContext;

			MultiBinding result = new MultiBinding();
			BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
			DependsOnAttribute[] deps = null;

			MemberInfo member, setBack = null;
			// if member is null/empty, use target name + property name [td]
			if ( String.IsNullOrEmpty( _member ) ) {
				string targetName = null;
				if ( @target is FrameworkElement )
					targetName = ( (FrameworkElement)@target ).Name;
				else if ( @target is FrameworkContentElement )
					targetName = ( (FrameworkContentElement)@target ).Name;
				string propertyName = valueProvider.TargetProperty.ToString();
				if ( !String.IsNullOrEmpty( targetName ) && !string.IsNullOrEmpty( propertyName ) )
					_member = targetName + "_" + propertyName;
			}

			if ( String.IsNullOrEmpty( _member ) )
				throw new Exception( "No member identified and no default member name could be determined. If you are using the shorthand syntax, be sure to assign an 'x:Name' attribute on the target object." );
			
			member = @this.GetType().GetProperty( _member, flags ); // properties have precedence (not that it matters)
			
			if ( member != null )
				result.Bindings.Add( new Binding( _member ) { Source = @this } ); // add implict dependency for base property itself (just like a normal binding)
			else
				member = @this.GetType().GetMethod( _member, flags, null, Type.EmptyTypes, null );

			if ( member == null )
				throw new Exception( String.Format( "No matching property or parameterless method with the name \"{0}\" could be found in type {1}", _member, @this.GetType().FullName ) );

			if ( !String.IsNullOrEmpty( SetBack ) ) {
				setBack = thisType.GetProperty( SetBack, flags );
				if ( setBack == null )
					// can we change this to look for any single-parameter method not on object type?
					setBack = thisType.GetMethod( SetBack, flags, null, new Type[] { typeof( object ) }, null );
			}

			deps = (DependsOnAttribute[])member.GetCustomAttributes( typeof( DependsOnAttribute ), false );

			if ( deps != null ) {
				bool contextDependency = false;
				foreach ( DependsOnAttribute dep in deps ) {
					string depString = dep.Dependency;
					string depTarget = depString.Split( new char[] { '.' }, 2 )[0];
					string depPath = depString.Split( new char[] { '.' }, 2 )[1];
					Binding depBinding;
					switch ( depTarget.ToLower() ) {
						case "this":
							depBinding = new Binding( depPath );
							depBinding.Source = @this;
							break;
						case "target":
							depBinding = new Binding( depPath );
							depBinding.Source = @target;
							break;
						case "context":
							contextDependency = true;
							depBinding = new Binding( "DataContext." + depPath );
							depBinding.Source = @target;
							break;
						default:
							object customTarget;
							depBinding = new Binding( depPath );
							if ( targetIndex.TryGetValue( depTarget.ToLower(), out customTarget ) )
								depBinding.Source = customTarget;
							else
								throw new Exception( String.Format( "Custom target \"{0}\" not registered. Call DefineExtension.AddTarget() before using this member.", depTarget ) );
							break;
					}
					depBinding.Mode = BindingMode.OneWay;
					result.Bindings.Add( depBinding );
				}

				if ( contextDependency )
					result.Bindings.Add( new Binding( "DataContext" ) { Source = @target } );
			}

			result.Converter = new LinkValueConverter( member, setBack );
			result.ConverterParameter = @this;
			result.UpdateSourceTrigger = UpdateSourceTrigger;

			//BindingOperations.SetBinding( (DependencyObject)@target, (DependencyProperty)@property, result );

			return result.ProvideValue( serviceProvider );
		}

		static LinkExtension() {
			AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler( CurrentDomain_UnhandledException );
		}

		static void CurrentDomain_UnhandledException( object sender, UnhandledExceptionEventArgs e ) {

		}

		public static void AddTarget( string name, object target ) {
			if ( name.Contains( "." ) )
				throw new Exception( "Target names cannot contain periods ('.')." );

			name = name.ToLower();

			if ( !targetIndex.ContainsKey( name ) )
				targetIndex.Add( name, target );
			else if ( targetIndex[name] != target )
				throw new Exception( "A different target has already been added under the same name." );
		}

		public static bool RemoveTarget( string name ) {
			name = name.ToLower();
			if ( targetIndex.ContainsKey( name ) ) {
				targetIndex.Remove( name );
				return true;
			}
			return false;
		}
	}
}
